{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 多GPU训练\n",
    ":label:`sec_multi_gpu`\n",
    "\n",
    "到目前为止，我们讨论了如何在CPU和GPU上高效地训练模型，同时在 :numref:`sec_auto_para`中展示了深度学习框架如何在CPU和GPU之间自动地并行化计算和通信，还在 :numref:`sec_use_gpu`中展示了如何使用`nvidia-smi`命令列出计算机上所有可用的GPU。\n",
    "但是我们没有讨论如何真正实现深度学习训练的并行化。\n",
    "是否一种方法，以某种方式分割数据到多个设备上，并使其能够正常工作呢？\n",
    "本节将详细介绍如何从零开始并行地训练网络，\n",
    "这里需要运用小批量随机梯度下降算法（详见 :numref:`sec_minibatch_sgd`）。\n",
    "后面我还讲介绍如何使用高级API并行训练网络（请参阅 :numref:`sec_multi_gpu_concise`）。\n",
    "\n",
    "## 问题拆分\n",
    "\n",
    "我们从一个简单的计算机视觉问题和一个稍稍过时的网络开始。\n",
    "这个网络有多个卷积层和汇聚层，最后可能有几个全连接的层，看起来非常类似于LeNet :cite:`LeCun.Bottou.Bengio.ea.1998`或AlexNet :cite:`Krizhevsky.Sutskever.Hinton.2012`。\n",
    "假设我们有多个GPU（如果是桌面服务器则有$2$个，AWS g4dn.12xlarge上有$4$个，p3.16xlarge上有$8$个，p2.16xlarge上有$16$个）。\n",
    "我们希望以一种方式对训练进行拆分，为实现良好的加速比，还能同时受益于简单且可重复的设计选择。\n",
    "毕竟，多个GPU同时增加了内存和计算能力。\n",
    "简而言之，对于需要分类的小批量训练数据，我们有以下选择。\n",
    "\n",
    "第一种方法，在多个GPU之间拆分网络。\n",
    "也就是说，每个GPU将流入特定层的数据作为输入，跨多个后续层对数据进行处理，然后将数据发送到下一个GPU。\n",
    "与单个GPU所能处理的数据相比，我们可以用更大的网络处理数据。\n",
    "此外，每个GPU占用的*显存*（memory footprint）可以得到很好的控制，虽然它只是整个网络显存的一小部分。\n",
    "\n",
    "然而，GPU的接口之间需要的密集同步可能是很难办的，特别是层之间计算的工作负载不能正确匹配的时候，\n",
    "还有层之间的接口需要大量的数据传输的时候（例如：激活值和梯度，数据量可能会超出GPU总线的带宽）。\n",
    "此外，计算密集型操作的顺序对于拆分来说也是非常重要的，这方面的最好研究可参见 :cite:`Mirhoseini.Pham.Le.ea.2017`，其本质仍然是一个困难的问题，目前还不清楚研究是否能在特定问题上实现良好的线性缩放。\n",
    "综上所述，除非存框架或操作系统本身支持将多个GPU连接在一起，否则不建议这种方法。\n",
    "\n",
    "第二种方法，拆分层内的工作。\n",
    "例如，将问题分散到$4$个GPU，每个GPU生成$16$个通道的数据，而不是在单个GPU上计算$64$个通道。\n",
    "对于全连接的层，同样可以拆分输出单元的数量。\n",
    " :numref:`fig_alexnet_original`描述了这种设计，其策略用于处理显存非常小（当时为2GB）的GPU。\n",
    "当通道或单元的数量不太小时，使计算性能有良好的提升。\n",
    "此外，由于可用的显存呈线性扩展，多个GPU能够处理不断变大的网络。\n",
    "\n",
    "![由于GPU显存有限，原有AlexNet设计中的模型并行](https://d2l.ai/_images/alexnet-original.svg)\n",
    ":label:`fig_alexnet_original`\n",
    "\n",
    "然而，我们需要大量的同步或*屏障操作*（barrier operation），因为每一层都依赖于所有其他层的结果。\n",
    "此外，需要传输的数据量也可能比跨GPU拆分层时还要大。\n",
    "因此，基于带宽的成本和复杂性，我们同样不推荐这种方法。\n",
    "\n",
    "最后一种方法，跨多个GPU对数据进行拆分。\n",
    "这种方式下，所有GPU尽管有不同的观测结果，但是执行着相同类型的工作。\n",
    "在完成每个小批量数据的训练之后，梯度在GPU上聚合。\n",
    "这种方法最简单，并可以应用于任何情况，同步只需要在每个小批量数据处理之后进行。\n",
    "也就是说，当其他梯度参数仍在计算时，完成计算的梯度参数就可以开始交换。\n",
    "而且，GPU的数量越多，小批量包含的数据量就越大，从而就能提高训练效率。\n",
    "但是，添加更多的GPU并不能让我们训练更大的模型。\n",
    "\n",
    "![在多个GPU上并行化。从左到右：原始问题、网络并行、分层并行、数据并行](https://d2l.ai/_images/splitting.svg)\n",
    ":label:`fig_splitting`\n",
    "\n",
    " :numref:`fig_splitting`中比较了多个GPU上不同的并行方式。\n",
    "总体而言，只要GPU的显存足够大，数据并行是最方便的。\n",
    "有关分布式训练分区的详细描述，请参见 :cite:`Li.Andersen.Park.ea.2014`。\n",
    "在深度学习的早期，GPU的显存曾经是一个棘手的问题，然而如今除了非常特殊的情况，这个问题已经解决。\n",
    "下面我们将重点讨论数据并行性。\n",
    "\n",
    "## 数据并行性\n",
    "\n",
    "假设一台机器有$k$个GPU。\n",
    "给定需要训练的模型，虽然每个GPU上的参数值都是相同且同步的，但是每个GPU都将独立地维护一组完整的模型参数。\n",
    "例如， :numref:`fig_data_parallel`演示了在$k=2$时基于数据并行方法训练模型。\n",
    "\n",
    "![利用两个GPU上的数据，并行计算小批量随机梯度下降](https://d2l.ai/_images/data-parallel.svg)\n",
    ":label:`fig_data_parallel`\n",
    "\n",
    "一般来说，$k$个GPU并行训练过程如下：\n",
    "\n",
    "* 在任何一次训练迭代中，给定的随机的小批量样本都将被分成$k$个部分，并均匀地分配到GPU上。\n",
    "* 每个GPU根据分配给它的小批量子集，计算模型参数的损失和梯度。\n",
    "* 将$k$个GPU中的局部梯度聚合，以获得当前小批量的随机梯度。\n",
    "* 聚合梯度被重新分发到每个GPU中。\n",
    "* 每个GPU使用这个小批量随机梯度，来更新它所维护的完整的模型参数集。\n",
    "\n",
    "\n",
    "在实践中请注意，当在$k$个GPU上训练时，需要扩大小批量的大小为$k$的倍数，这样每个GPU都有相同的工作量，就像只在单个GPU上训练一样。\n",
    "因此，在16-GPU服务器上可以显著地增加小批量数据量的大小，同时可能还需要相应地提高学习率。\n",
    "还请注意， :numref:`sec_batch_norm`中的批量规范化也需要调整，例如，为每个GPU保留单独的批量规范化参数。\n",
    "\n",
    "\n",
    "## 小结\n",
    "\n",
    "* 有多种方法可以在多个GPU上拆分深度网络的训练。拆分可以在层之间、跨层或跨数据上实现。前两者需要对数据传输过程进行严格编排，而最后一种则是最简单的策略。\n",
    "* 数据并行训练本身是不复杂的，它通过增加有效的小批量数据量的大小提高了训练效率。\n",
    "* 在数据并行中，数据需要跨多个GPU拆分，其中每个GPU执行自己的前向传播和反向传播，随后所有的梯度被聚合为一，之后聚合结果向所有的GPU广播。\n",
    "* 小批量数据量更大时，学习率也需要稍微提高一些。\n",
    "\n",
    "## 练习\n",
    "\n",
    "1. 在$k$个GPU上进行训练时，将批量大小从$b$更改为$k \\cdot b$，即按GPU的数量进行扩展。\n",
    "1. 比较不同学习率时模型的精确度，随着GPU数量的增加学习率应该如何扩展？\n",
    "1. 实现一个更高效的`allreduce`函数用于在不同的GPU上聚合不同的参数？为什么这样的效率更高？\n",
    "1. 实现模型在多GPU下测试精度的计算。\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Java",
   "language": "java",
   "name": "java"
  },
  "language_info": {
   "codemirror_mode": "java",
   "file_extension": ".jshell",
   "mimetype": "text/x-java-source",
   "name": "Java",
   "pygments_lexer": "java",
   "version": "14.0.2+12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
